The aim of the NLB Framework is to provide the infrastructure required to create, load, view, interact with, analyse and save a detailed Building Information Model (BIM), and do so in a way that is as simple, intuitive and flexible as possible. It is designed to support a range of different host applications that use some or all of its classes, and may have very different needs and requirements. For example, the core schedule editing application has no requirement for 3D in any way, so should be able to access only the classes it needs without having to load any of the 3D modelling, scene editing, view management or even point-based utility classes. Similarly, various web workers may need to load very isolated parts of the framework to do just their specific job. As a result, a lot of work has been done to facilitate and support this kind of isolation and sub-setting.
Before getting into the details, it is important to explain the layout of classes within the src subfolder in the NLB code repository, so that you will at least be able to find things when you need to:
apps: Some core applications that share the framework,assets: Images and data files referenced by the framework,css: Common style sheets used by core applications and SVG classes,lib: Third-party libraries used by core applications and the framework,pd: Subfolder containing allPD,BIMand other framework classes,app: Application support classes providing state, actions and undo/redo,bim: Classes that define the various BIM elements and entities that populate a model,core: Core PD classes that make up the framework and do most of the low level work,ctrl: Control classes that define interface with the system APIs such as clipboard, mouse and video,occ: Classes for interacting with the OpenCascade geometry kernel WASM module,sim: Classes for simulating, analysing and visualising model metrics and behaviours,svg: PD.SVG classes for generating charts and graphs within the UI and for export, andvue: PD.Vue Classes defining common components used in the UI of core applications.
refs: Additional markdown documentation included in JSDocs as tutorials,
For third-party application developers, the classes defined in the pd/app subfolder will be the most relevant as they are what your application will very likely interface with the most. Thus, the following is an overview of the main application support classes and a description of what they do and what services they provide.
Overview and Philosophy
The primary role of a host application is to provide a user interface that exposes all the actions that a user may perform in their creation of and/or interaction with a model, and at least one work area in which to carry out those actions. For a general BIM application, this may be a full 3D WebGL model canvas on the main page together with menus, icons and panels that expose virtually all available actions provided by the framework. For a much simpler chart-based application, this may be a single SVG container on the main page and some controls for changing state specific to that chart. A shading-focussed application may restrict the user to a single structure with a single level containing a single wall with a single aperture, and provide an assortment of shading devices that can be applied to it and tested.
The application support classes in the pd/app subfolder have been designed as much as possible to support each of these very different application requirements, and any other variants in between. However, there are some key concepts and requirements that are common to all host applications.
Global Static Objects
One of these key concepts is the use of a number of global static object literals within the framework that are used to group together specific functionalities. The term global is used here within the context of global variables. These static objects provide application-wide access to global state, methods for changing global state, methods for managing the global undo/redo queue, and methods for subscribing to and dispatching application-wide events. As this code seeks to be UI framework agnostic, these global static objects are used on lieu of a ready-made store such as Vuex or Flux.
These global static objects are described in more detail below and all exist within the PD namespace.
Reactivity and Observability
To support reactivity and observability within whatever UI framework is being used, the NLB framework also provides proxy aliases for some of these global objects. By default, these are simply aliases that reference the original global static object. Even though the NLB framework reads data from the original global static objects, it is very careful to only ever write data to the proxy aliases. If your UI does not need reactivity, then by default the framework reads and writes to the same global static object so you don't need to do anything.
However, if your application needs to observe changes in global state and view values, then it will need to reassign or overwrite these proxy aliases using whatever means the particular UI framework being used provides.
It probably doesn't need saying, but the primary benefit of using of global static objects and proxies is that you can add several different ways of setting the date/time, for example, and as long as they all observe the global date/time object, they will all update simultaneously when the user changes any one of them. This is important if you have toolbar shortcuts that replicate the functionality of components in a side panel. As they may both be visible in the UI at the same time, having them both update simultaneously is important visual feedback for the user as a way of promoting discoverability within the application.
Core Namespaces
The NLB Framework defines two core namespaces:
-
PD: Provides the lower-level supporting infrastructure for any interactive application that displays, edits and/or interacts with the BIM classes. It provides global state, actions, events and undo/redo, as well as a wide range of utilities for working with THREE.js and front-end frameworks such as Vue, React, Knockout and Enyo. It also provides the fundamental geometry creation, mesh generation, scene visualisation and user interaction management tools. -
BIM: Provides all the higher-level entities, elements and components for defining buildings and structures, and the fundamental hierarchy of classes that make them identifiable and navigable.
PD.GlobalState
The framework uses a static PD.GlobalState plain old Javascript object (POJO) to store application-wide state values. This is defined in the pd/app/pd-global-state.js file and includes things like site location and the current date/time for computing shadows, transient solar phenomenon and seasonal or time-based events, as well as status messages, color schemes and undo/redo status.
There is an additional pd/app/pd-global-state-3d.js file for 3D applications which augments global state with settings for the main model scene (typically displayed in a WebGL canvas within the main work area) such as model metrics, current user mode, navigation mode, snap, dimension and cursor settings and pointer actions.
It is a highly recommended practice that PD.GlobalState remain a Plain Old JavaScript Object (POJO), storing only simple data and having no methods added. This is because its data is shared between the NLB Framework and the host application's user interface. If the host application uses a javascript framework that automatically adds reactivity to the objects it uses, and a model object is added inadvertently to global state, you can see how easy it would be for that reactivity to quickly propagate throughout the model and add all sorts of unnecessary overhead. Thus, global state should only ever store numbers, strings and simple JSON-compatible objects such as dates, times and locations, or arrays of these.
PD.GlobalView
The pd/app/pd-global-state-3d.js file also includes a static PD.GlobalView object that defines the current view and display settings within the main model scene. The reason PD.GlobalView is a separate object from PD.GlobalState is that an application may have more than one scene canvas that may display entirely different views. Thus, this object may only refer to one of those scenes or the application may decide to update PD.GlobalView whenever the user's focus moves from one scene viewer to the other. The values in PD.GlobalState still apply equally to both scenes, but having PD.GlobalView separate and providing static methods in PD.View to support copying view data allows this extra flexibility.
Also, the data stored in PD.GlobalView directly mirrors that in the PD.View class, which makes copying, creating and switching between stored PD.DrawingView instances much simpler.
PD.GlobalActions
The preferred way of changing state and view settings is by calling methods on the static PD.GlobalActions object defined in pd/app/pd-global-actions.js. Again, there is an additional pd/app/pd-global-actions-3d.js file for 3D applications that augments global actions with methods for changing model metrics, the current user mode, navigation mode, dimension and cursor settings, pointer actions and the current view.
This static PD.GlobalActions object is used by the framework itself when it needs to modify state, display messages or trigger UI updates, and you are encouraged to add your own custom methods to it if your application requires.
When creating custom methods on PD.GlobalActions, some of them will need to make actual changes to state. However, it is critically important that you consider the properties on PD.GlobalState and PD.GlobalView objects to be immutable. If you have to change state, you must only set values on the corresponding PD.GlobalStateProxy and PD.GlobalViewProxy objects.
Mutating and Monitoring State
If you look in the pd/app/pd-global-state.js and pd/app/pd-global-state-3d.js files, you will see that there are also static PD.GlobalStateProxy and PD.GlobalViewProxy objects defined. By default, these directly reference PD.GlobalState and PD.GlobalView. However, they provide host applications with an opportunity to create proxies that monitor and/or intercept global state and view changes so that they can handle related actions and automatically update their UI in response.
Even if your application does not reassign PD.GlobalStateProxy and PD.GlobalViewProxy, it is a highly recommended practice that you only mutate global state and view settings via these proxies as it allows you to change your mind at a future time, and also makes it much simpler to search your code for instances where a property is set to a new value rather than just read.
Beyond that, it is also a highly recommended practice that, wherever possible, you only make state changes inside methods that you add to PD.GlobalActions. You should follow the pattern(s) used in the existing global actions files, as per the example code shown below. This provides the host application with two methods, one allowing it to set the value directly and the other providing support for a toggle button or icon. Even in this example, the actual state change is only made in one location inside PD.GlobalActions, but is available to the entire framework.
/**
* Sets the `PD.GlobalView.showDraftingLines` flag
* and updates the model.
*
* @param {boolean} [state] The new state to set, defaults to `true`.
*/
PD.GlobalActions.showDraftingLines = function(state) {
state = PD.Utils.toBoolean(state, true);
if (PD.GlobalView.showDraftingLines != state) {
PD.GlobalViewProxy.showDraftingLines = state;
PD.GlobalActions.rebuildVisibleLevels();
}
};
/**
* Toggles the `PD.GlobalView.showDraftingLines` flag
* on/off and updates the model.
*/
PD.GlobalActions.toggleDraftingLines = function() {
PD.GlobalActions.showDraftingLines(!PD.GlobalView.showDraftingLines);
};
PD.GlobalUndo
Having a robust system for undoing and redoing actions within a BIM application is a complex but necessary requirement. The NLB Framework provides undo/redo infrastructure via the static PD.GlobalUndo and PD.UndoActions classes which are defined in the pd/app/pd-global-undo.js file. The PD.GlobalUndo manager maintains the global undo/redo lists, updates global state when either list changes and provides methods for recording the model and UI actions that can be undone and redone. There is also a pd/app/pd-global-undo-3d.js file for 3D applications that augments global undo with additional actions for changing the model as well as 3D view and display settings.
The PD.UndoActions object is basically a storage point for core undo/redo action handlers. All of the framework's undo/redo action handlers are stored there and you are encouraged to add your own custom undo/redo handlers there too. All undo/redo action handlers are classes that inherit from PD.UndoAction.
PD.UndoAction
The PD.UndoAction class is the core of the undo/redo system. All undo-able and redo-able actions must have a corresponding subclass that does whatever is necessary within the framework to undo an action and redo it if required. This means that instances of the subclass must have a constructor that takes and stores all the information required for the action based on the current state of the model.
The framework has to manage a diverse range of user interactions with the model, some of which can be highly dynamic and some of which can occur in unpredictable sequences. For example, a host application may offer the user a set of buttons to increment or decrement a parameter value. If the user clicks one of the buttons, this needs to be recorded as an undo/redo action. However, if the user clicks the same button several times in quick succession, this should not generate several copies of the same undo/redo action in the list, but each subsequent click should instead update the previous action of the same type.
Which undo/redo actions should do this and which should not really has to be up to the host application. The following example code shows how you might do this with your own subclasses.
/**
* Records a change in an element's path fill color.
* @extends PD.UndoAction
* @author drajmarsh
* @private
* @class
*/
PD.UndoActions.PathColorChange = class extends PD.UndoAction {
/**
* Creates a new element path fill color change record.
*
* @param {PD.Element} element The element whose path color changed.
* @param {THREE.Color|null} oldColor The previous color value before the change.
* @param {THREE.Color|null} newColor The previous color value before the change.
*/
constructor(element, oldColor, newColor) {
super('Change Path Color - ' + element.name);
// Convert to arrays in case original objects change.
this.newColor = newColor ? newColor.toArray() : null;
this.oldColor = oldColor ? oldColor.toArray() : null;
this.element = element;
};
// ----------------------------
// Overridden Methods.
undo(undoManager) {
this.setPathColor(this.element, this.oldColor);
};
redo(undoManager) {
this.setPathColor(this.element, this.newColor);
};
// ----------------------------
// Static Method(s).
/**
* Checks for a valid path, sets its color and updates the model.
*
* @param {PD.Element} element The element whose path color changed.
* @param {number[]} color The color to set the element path to.
* @static
*/
static setPathColor(element, color) {
if (element?.path) {
if (!color) element.path.fillColor = null;
else PD.Utils.copyColor(color, element.path.fillColor);
PD.GlobalActions.rebuildModel();
}
};
};
/**
* Change the color of an element's path and create an undo/redo record.
*
* @param {BIM.Element} element The element whose path color is to be changed.
* @param {THREE.Color|null} newColor The new color value to change the path to.
*/
PD.GlobalUndo.setAndRecordPathColorChange = function(element, newColor) {
if (element.path != null) {
if (PD.GlobalUndo.isActive) {
// Check to update previous record.
const last_item = PD.GlobalUndo.getLastUndoActionOfTypeWithinTime(PD.UndoActions.PathColorChange);
if (last_item && (last_item.element == element)) {
last_item.newColor = newColor;
last_item.timestamp = Date.now();
}
// Create new undo record.
else PD.GlobalUndo.addActionToUndoQueue(
new PD.UndoActions.PathColorChange(element, element.path?.fillColor, newColor)
);
}
// Use static method on `PathColorChange` to set the
// color after old and new colors have been recorded.
PD.UndoActions.PathColorChange.setPathColor(element, newColor);
}
};
The above code first creates a subclass of PD.UndoAction with a unique name on PD.UndoActions. Its constructor takes all the values that it needs for the overridden undo/redo methods to be able to do what is needed entirely independently when called. In the spirit of DRY, it also provides a static method that does the actual work for both undo and redo.
The second thing the code does is add a method to PD.GlobalUndo that checks to update an existing or add a new instance of the subclass to the global undo/redo queue and then actually makes the change. It is not always necessary or appropriate for such methods to actually make the change. There are many core undo/redo actions in the framework that are used to record a change that is about to or has already happened. However, it can sometimes significantly reduce the overall amount of code in your host application if such methods can work out for themselves what the previous value(s) were rather than you having to do it prior to each invocation, which may require the method to make any changes itself.
Now, any of the method(s) you have added to PD.GlobalActions can call this undo method whenever they need to change an element's path color and it will be an automatically undo-able and redo-able action.
PD.GlobalEvents
The framework uses a static PD.GlobalEvents object, defined in the pd/app/pd-global-events.js file. This static object publishes a range of PD.EVENT types that your host application, as well as any custom elements and components you create, can subscribe to in order to respond appropriately to application-wide events.
As an example, the following code shows how your host application may subscribe and respond to changes between light and dark mode within its UI.
PD.GlobalEvents.addEventListener(PD.EVENT.DARK_MODE, (dark) => {
UI.setMyDarkModeToggleButtonState(dark);
UI.changeMyCustomCursorColors(dark);
UI.redrawMyCustomSVGCharts(dark);
});
Events are one of the key mechanisms for ensuring that the NLB Framework is kept entirely independent of and isolated from the front-end user interface (UI) framework. It allows frameworks such as Vue, React, Preact, Angular, Enyo, Ember, Knockout or any other to subscribe to global events and handle them in any way they wish without the NLB Framework needing to know anything about how or when that is done.
Whilst modern UI frameworks no longer pose the same kinds of risk, some frameworks are/were quite invasive when adding observability or reactivity to objects, sometimes recursing down many levels and actually modifying properties or the objects themselves. Using pub/sub message passing is a way to try to minimise the risk of this kind of unwanted 'leakage' of reactivity or observability into the actual BIM model.
Manager Classes
If the host application allows the user to interact with and edit a 3D model, then it will need to initialise the 3D model management services provided by the NLB Framework. This is actually quite straightforward and requires that you include something akin to the following code once all framework files have been loaded.
const nlb_container = document.getElementById('my-work-area-id');
const model = PD.GlobalActions.createModel(nlb_container);
The PD.GlobalActions.createModel method creates instances of the PD.SceneManager, PD.ModelManager and PD.SelectionManager classes to manage rendering, selection and editing the model in the main scene. The method also takes a configuration object that allows you the option of passing in your own custom subclasses of any of these management classes should you need to. This method returns the instance of the PD.ModelManager used for the main scene so that you can initialise the model and add elements immediately after if you need to.
The instances of each manager class in the main scene are also available globally via the PD.GlobalActions.sceneManager, PD.GlobalActions.modelManager and PD.GlobalActions.selectionManager getters.
The nature of and relationships between these three manager classes are described in the Interactive Selection section.